In a previous article, we discussed how to enable an application with Azure AD without making any implementation changes that integrate directly with Azure AD API. With that type of integration, we saw how all of our content is protected under the Azure security platform. We also noticed that there is a limitation when we need to provide anonymous access to some of the resources. In this article, we take a look at how to address this limitation by taking a direct control of the security in our application.
To download the code, clone the repo from Github with the following command:
git clone https://github.com/ozkary/nodejs-azure-ad.git
|
This creates a new folder (nodejs-azure-ad) and downloads the code. The code for this article is in the master branch. The simple-auth branch has the code for the first article in which we are only enabling the authentication for the entire app.
Nodejs Passport Azure AD Authentication
We now need to enable the security to allow anonymous requests. We start by looking at the options that are available on Azure. Let’s get back to Azure and select our web app. Click on Select Settings -> Authentication / Authorization and click on On for the App Service Authentication option. This enables additional options for the actions to take when a request is not authenticated. We can select either of the following options:
- Log in with Azure Active Directory
- Allow Anonymous requests (no action)
The first option forces a redirect and does not allow the option to provide anonymous access to any of the app content. When we need to provide anonymous access to some of our application resources, we need to select the second option which allows anonymous requests to pass through. This passes the authorization concerns to our application logic.
Handling Authorization
After we enable the option to Allow Anonymous requests, the application is enabled to receive all the requests. It is the responsibility of the application to determine what should be protected using authorization filters which in turn can redirect the traffic to an identity provider like Azure AD. The identity provider is the system that presents the login view to the users.
Install the supporting libraries
In order to implement our authorization logic, we need to integrate directly with Azure AD. Lucky for us, there are plenty libraries that can help enable our integration. Let’s start by installing a few of those libraries in our application. Run the following command from the command line in the working directory where the code has been cloned.
Note: These libraries have already been configured on the package.json file, but these steps are shown to clearly show what libraries are required for this integration.
npm install –save passport –save
|
The passport library helps us authenticate the requests by leveraging several strategy plugins to delegate the authentication of users over different protocols. For more information on this library visit: https://www.npmjs.com/package/passport
npm install passport-azure-ad-oauth2 --save
|
We also install the passport azure ad oath library which is the strategy that password uses to provide the authentication over the OAuth 2.0 protocol which is supported by Azure AD. For more information visit: https://www.npmjs.com/package/passport-azure-ad-oauth2
npm install express-session --save
|
The last library to install is needed for session management. This is what passport uses to manage the persisted user information.
Authorization Module
Now that we have the libraries installed, we can move forward with the implementation of our custom authorization module. The approach is to initialize the OAuth strategy plugin with our Azure AD application settings which can be obtained from Azure. We need the following information:
ClientID
|
This is our application id
|
Client Secret
|
This is an app key that can be generated from azure and that can be set to expire. See next section for more information
|
Callback Url
|
This the url that the identity provider service can use to send back the response after the user authenticates.
|
Generate App key/secret
To generate the app key, we need to go back to azure and visit the AD app settings: Azure Active Directory->Application Registration -> APP Name->Settings->Keys.
We can now type the key description and set the duration. The key value is generated automatically after we save the settings. We need to copy the key information before moving away from this blade as described in this next image: (The key displayed on the image is no longer valid)
From the settings blade, we click on the Properties option. This is where we can find the application id and return Url. After we collect our app information, we can now initialize our passport strategy with the following code in our modules/auth.js file: (replace #s with your app information)
var passport = require('passport');
var oauthStrategy = require('passport-azure-ad-oauth2').Strategy;
module.exports.init = function(app, $users)
{
var strategy = new oauthStrategy({
clientID: '####', //app id
clientSecret: '####', //your app key
callbackURL: 'https://####.azurewebsites.net/onauth',
},function(accessToken, refresh_token, params, profile, done){
//decodes the token and sends the information to the user profile handler
var context = profile;
done(null, context);
});
...
}
|
The code above sets the app information, so the strategy plugin can provide the app registration to the Azure identity provider. In the callback url parameter, we need to add a route, so we are able to write a handler for the callback. In this case, we add the onauth route for which a handler is implemented by our modules/route.js module which is cover in a coming section.
The anonymous method on the oauth strategy is called when the authentication is successful. It receives the profile information which is a Jason Web Token (JWT). By calling the done callback with the user context/profile, we are passing the execution to the strategy userProfile handler which takes care of parsing the token and reading the claims from the token. This is the area of the code where we need to transform JWT format into our custom user context JSON format.
Note: The authentication process is started by calling password authenticate in our authorization filter code. We can see this implementation in the Authorization Filter section.
//user profile handler to parse the data and create the user profile
strategy.userProfile = function (accessToken, done) {
var profile = {};
try {
var tokenBase64 = accessToken.split('.')[1];
var tokenBinary = new Buffer(tokenBase64, 'base64');
var tokenAscii = tokenBinary.toString('ascii');
var tokenObj = JSON.parse(tokenAscii);
profile.id = tokenObj.upn;
profile.email = tokenObj.upn; //upn is the email on AD
profile.displayname = tokenObj.given_name + ' ' + tokenObj.family_name;
done(null, profile);
console.log('user profile', profile);
} catch (ex) {
console.log("Unable to parse oauth2 token from WAAD.");
done(ex, null);
}
};
|
Once our user context is created, we call the done callback with the custom model. This passes execution to the passport serialize user function which is the area where we should add the user information to a session storage. In our example, we are using in-memory hash table to illustrate the process.
//writes to local session
passport.serializeUser(function (user, done) {
$users[user.id] = user;
done(null, user.id);
});
//gets from local session
passport.deserializeUser(function (id, done) {
var user = $users[id];
done(null, user);
});
|
App Permissions
We also need to set the app permissions to access the Microsoft Graph and be able to read the directory data. If this permission is not set, the app would not be able to have access to the Microsoft Resource identifier. To enable the app permissions, we need to go back to azure and visit the AD app settings: Azure Active Directory->Application Registration ->APP Name->Settings->Required Permissions. From there we need to click on Add and select Microsoft Graph. This enables other settings for which we need to check Read directory data as show below:
On our server code (modules/auth.js), we need to add the handler for the passport strategy token params. This sets passport with the resource Url which is used to get the authentication token.
//Azure resource you're requesting access
//prevents the Resource identifier is not provided error
strategy.tokenParams = function(options) {
return { resource: 'https://graph.windows.net' };
}
|
Authorization Filter
The deserialize user function is used by the passport session management process. The main purpose is to fetch the user context from a local cache to set the request as an authenticated request. This is what enables our authorization filter with the session/user context to allow access to the authorized areas of our application.
//passport authorization filter
passport.authorize = function authorize(req, res, next) {
var auth = req.isAuthenticated();
if (req.isAuthenticated() || req.user) {
return next();
}
//authenticate the user
passport.login()(req, res, next);
}
//login extension method to validate the login state
passport.login = function () {
return passport.authenticate(azureOAuth,
{ failureRedirect: '/', failureFlash: true });
}
|
When the request is authenticated, we just return next which allows the request to continue. If no user context was found on the current request, we force the user to login by calling passport authenticate. This is the entry point for the passport authentication management process which consists of a series of redirects to the Single Sign On page presented by the identity provider system.
Application Routes
Now that we have the authorization filter ready, we need to secure our application routes. This is done by setting the filter on any route that should be under authentication. (modules\route.js)
//secured api routes with no redirect
function authorizeApi(req, res, next) {
var auth = req.isAuthenticated();
if (req.isAuthenticated()) {
return next();
} else {
res.send(401, 'Not authorized');
}
}
//authorize the routes
app.use('/api/user', authorizeApi);
app.use('/login', passport.authorize);
//add the route handlers
app.get('/api/user', profile)
app.get('/login', login)
//validate that the user profile is set on the authSession cookie
function profile(req, resp) {
resp.json({ session: req.user });
}
function login(req, res) {
res.sendFile(__dirname + "/app/index.html");
}
//route handler for the post authentication from identity provider
app.get('/onauth', passport.login(),
function (req, res) {
login(req, res);
}
);
|
We have two protected routes login and api/user.
Login Route
The login route starts the login process for the user. If the user is not logged on, the application uses the passports authorize filter to start the login process via HTTP redirects. This route is called from the login button on the client application.
api/user Route
This API route fetches the user information only when the user is logged on. If there is no active session, a HTTP 401 (not authorized) is returned. This is call every time the application loads to hide the login button and display the user content as shown below:
We should also note the onauth route which is our handler for the response from the identity provider after a successful login. On the handler, we call the passport login function to process the information and transform the token into our user context via the serialize user passport function.
Summary
We are now able to compare two possible integrations with Azure AD. When we select Login with Azure AD, we discover that Azure protects our application entirely and does not allow for any type of anonymous access to our resources.
In some cases, we need to provide the anonymous access and only protect some of the resources like API and/or server side controllers. For that scenario, we show how to select the Allow Anonymous Requests configuration option, use the passport libraries and Azure OAuth integration to take more direct control of our security.
We should note that the integration approach to take should be based on our application requirements.