4 minute read

I haven't touch Node.js much lately, however, back while I have been working with it, I was always curious, how to leverage both Passport.js with Azure AD and using ADAL for Node.js together in order to have ADAL handle the tokens, refreshes, cache etc. In the end, I have come up with a solution which I am going to share below.

So first off, you need to initialize Passport.js to use the OIDC strategy from passport-azure-ad package:

passport.use(new OIDCStrategy({
    callbackURL: process.env.returnUrl,
    clientID: process.env.clientId,
    clientSecret: process.env.clientSecret,
    validateIssuer: true,
    identityMetadata: "https://login.microsoft.com/thenetw.org/.well-known/openid-configuration",
    skipUserProfile: true,
    responseType: "code id_token",
    responseMode: "form_post",
    passReqToCallback: true,
}, function verify(req, iss, sub, profile, jwtClaims, accessToken, refreshToken, params, done) {
    if (!profile.id) {
        return done(new Error("No valid user auth ID"), null);
    }

    profile.initialRefreshToken = refreshToken;
    profile.oid = jwtClaims.oid;
    done(null, profile);
}));

Notice, that we save the refreshToken into the user's profile property as initialRefreshToken. This is quite important, because next, we are going to use it with ADAL for Node.js in order to exchange it for an actual access token. So next step is to initialize ADAL for Node.js:

const authContext = new AuthenticationContext("https://login.microsoftonline.com/thenetw.org", null, new MemoryCache());

Note that we are initializing it with a MemoryCache so that our credentials persist. The biggest issue with ADAL is that it doesn't cache tokens retreived by refresh token by default (maybe an idea for a pull request?), so we have to do a little workaround to force it into the cache.

// I suggest offloading this code to a separate .js script since it won't work with @ts-check or in TypeScript

const TokenRequest = require('./node_modules/adal-node/lib/token-request');

function obtainToken(user, resource, callback) {
    if (user.initialRefreshToken != undefined) {
        // Token has not been obtained by ADAL for Node.js, try to obtain it
        authContext.acquireTokenWithRefreshToken(
            user.initialRefreshToken,
            clientId,
            clientSecret,
            resource,
            function (error, result) {
                if (error) {
                    return callback(error);
                }
                else {
                    user.initialRefreshToken = null;
                    var tokenRequest = new TokenRequest(authContext._callContext, authContext, clientId, resource, null);
                    // Always refer to user by their objectId, this is useful when creating multi-tenant applications which support switching tenants
                    result.userId = user.oid;
                    return tokenRequest._addTokenIntoCache(result, callback);
                }
            });
    } else {
        // Token has been already obtained and is in memory cache, use it to obtain access token
        authContext.acquireToken(resource, user.oid, clientId, function (error, result) {
            if (error) {
                return callback(error);
            }
            else {
                return callback(null, result);
            }
        });
    }
}

So how does this piece of code work? First, we have to include the token-request.js in order to be able to access the token cache easily. Then, we take a look if this is our initial sign in - we have to exchange the refresh token for an access token and cache it or not. In case of having to create the entry in the cache, we have to  create a TokenRequest object, initialize it and then call  which does all the heavylifting. In the sample, I also slightly modify the initial token response to identify the user by their objectId within the Azure AD rather than using their userPrincipalName (note that if you are making a multi-tenant application and sharing the token cache, using objectIds or userPrincipalName + tenantId as an identifier is required for the cache to work properly). Once stored, every next request for the token goes through the cache (notice passing in the user.oid as user identifier - see explanation above).

The major difference between this approach and using ADAL with OpenID Connect Middleware in ASP.NET Core is that in case of Node.js the authorization code is redeemed for access and refresh tokens directly by the Passport.js (equivalent of OIDC middleware in ASP.NET Core) and then the refresh token is used to initialize ADAL where in ASP.NET Core, the authorization code redemption is already handled by ADAL. Either way, this is quite an obscure solution (yeah, accessing properties meant to be private is never good but it makes it work fine).

Comments

Lucky

This is one the problem I am facing for a long time and there is no absolutely no one who has combines the 2 of them right now. Your sample seems promising, perhaps can you share the code sample for it? It will be really helpful!! Thanks

Jan Hajek

That’s a great idea, I will try to make some sample app (no promises), but I think the sample in the post should be enough for usage, isn’t it?

Lucky

Well, I am really new to NodeJS development and recently found about passport, not sure how does it work. Might be a really silly question to ask, but how do we manage users using both the approach? in some cases I would like my front end to make the direct call to the resources and in some cases my rest api, will need to make the call on behalf of the user. if they both get the tokens independently I am not sure how it will work or what security issue it might present. Right now I have the following setup nodejs express back end api protected by aad nodejs web server serving static web pages and in all I have to use 3 resources from aad. mu backend api, sharepoint api and email api management of access tokens for all 3 of them from both the places seems a tedious approach. I feel I am not seeing the obvious here, should be some cleaner way to do it.

Jan Hajek

So in your case, I would do a following setup: a Single Page Application (SPA) to handle the front end, then your custom API and Micrsooft Graph. The SPA will authenticate the user directly (sample here https://github.com/Azure-Samples/react-aad-msal) and communicate with your API and MS Graph by passing the respective tokens for the services. On your custom API, you just validate the token and make sure that it contains the scope etc. and that the user is authorized to make the operation. In case you need to call other APIs from your own API, you can do so through “on-behlaf-of” flow - ADAL does the heavy lifting for you.

Lucky

I have already secured my backend api using azure-ad-jwt, it allows requests only with a valid token. thats done. regarding the sample you provided, I am little confused here. So will my SPA use passport_adal , adal node and aad-msal?? Feels passport is not needed in the front end app. Also access tokens for the 3 resources will be managed automatically by adal node? I dont want to use graph api as it doesnt exposes all functioanlities of sharepoint as of now. will have to explicity use 3 resource. And I have approx 10 pages in my front end, how can I verify before going to each of the page that user is already authenticated? do we have a function in adal node which verifies if user is already logged in or so?

Jan Hajek

So in case you make a Single Page App, you wouldn’t use Passport.js on frontend and instead use authentication within the SPA framework you use. For example ADAL for React, Angular etc.

There is a difference between ADAL and MSAL - basically, MSAL is a successor of ADAL. You should be using MSAL nowadays when starting a new project unless you hit some big issues - haven’t hit any myself yet. You can use ADAL and MSAL in the project (not at the same time tho) at different tiers of application - for example SPA with MSAL, backend with ADAL etc.

sercangurbuz2017sercan

Hi Jan

I have configured Azure Ad as Web App and try to get authtenciated with passport and adal-node.I followed the same pattern you defined here.I have two problem here with acquireToken call.

First there is conflict with api versions explained here https://github.com/AzureAD/azure-activedirectory-library-for-nodejs/pull/149

spn: prefix is added to resource and i got no cached entry error because resource is not prefixed on cached version.

This problem is solved removing the version assignment line indicated in the PR.

Second,When cached version of accesstoken is expired,acquireToken function try to refresh requesting the token endpoint without clientSecret which is an error.

As your app is configured in Azure as WebApp, there must be client secret. This is the error i faced.

“AADSTS70002: The request body must contain the following parameter: ‘client_secret or client_assertion’.

Any idea ?

Jan Hajek

From the source of the library, it doesn’t seem to require not include the refresh token in the response (https://github.com/AzureAD/azure-activedirectory-library-for-nodejs/blob/81ff31dd612cae6cd69a9a7452171b7af792be9f/lib/token-request.js#L118) which could be a bug in this case - since client_secret is required when refreshing a token: https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code#refreshing-the-access-tokens

Also, I think you best bet would be to obtain the tokens from the cache, checking expiration and if they expired, calling getTokenWithRefreshToken manually with obtained refreshToken from cache and you client secret. I can try to update the sample if it would be any useful for you.

Aditionally, I would suggest moving away from using this application model, and switching either to SPA + API or some similar architecture since such backend authentication and advanced flows seem to be rather complicated with ADAL for Node.js.

I have modified this reply multiple times as ideas were flowing through my head

To submit comments, go to GitHub Discussions.