In this demo you will extend the application from the previous demo 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();
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.
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();
});
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.
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.
Now that you've added authentication, you can continue to the next module, Extend the Node.js Express app for Microsoft Graph.