Complete guide to Azure AD Authentication in Node

There is a lot of documentation available on how to authenticate users against Azure AD. There are even libraries like MSAL.js that are supposed to do the job for you. Unfortunately, for me, I found the Microsoft documentation very noisy and confusing. Which library am I supposed to use? MSAL.js, ADAL.js, there is even a Passport.js in classic Microsoft fashion of developing multiple products for same use case – think Yammer, Teams, Skype or OneDrive, Sharepoint. Should I do OAuth or OpenIDConnect? It does not stop here. Microsoft provides two endpoints for doing OAuth – v1 and v2.

The link that is most helpful to understand and get the background on doing authentication using OAuth or OpenIdConnect (OIDC) is https://developer.okta.com/blog/2019/10/21/illustrated-guide-to-oauth-and-oidc. Start by reading this link and understand the four things you need in Azure before you can proceed further:

  • Client ID: identifier of your application. Also known as Application ID. You will get this by creating a new App Registration in Azure.
  • Tenant ID: identifier of the organization’s Azure AD against which you are trying to authenticate users.
  • Client Secret: password of your application. You create a new client secret in Azure portal.
  • Redirect URL: this has to be setup in Azure as well.

The OAuth authentication is a two step process. We use v1 endpoints in this article.

There are two endpoints involved in OAuth authentication. An authorization endpoint and a token endpoint.

1. In the first step, one makes a request to the authorization endpoint. A complete request might look like:

https://login.microsoftonline.com/109156be-c4fb-41ea-b1b4-efe1671c5836/oauth2/authorize/?client_id=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d&response_type=code&response_mode=query&redirect_uri=https://foo.com/auth/callback&prompt=consent&state=https://foo.com/the/page/user/wanted/to/access

This endpoint will take the user to Microsoft login page. The user logs in. Then user is prompted to grant permissions to the app. If user approves, an access code is returned to the user’s browser and the browser is redirected to the redirect_uri – think of this as a callback url. The point to note here is that it is the user’s browser which will invoke the redirect_uri with the access code, not Azure. Azure does not call the callback. Next thing to note is that Azure only accepts https URL for the redirect_uri and this restriction comes from the protocol itself because of security considerations [https://tools.ietf.org/html/rfc6749#section-10.3]

An example callback might look like

https://foo.com/auth/callback?code=AQABAAIAAAAm-0gAA&state=%2f&session_state=790c56da-2615-484b-9200-54d08cb21482

Whatever you pass in the state parameter in the call to authorize, you will get it back in the callback. You should set the state parameter to the URL the user was trying to access in the first place. Otherwise, that information will be lost and you won’t be able to land the user to appropriate page after logging in the user.

2. In your callback method, you use the access code to get access token of the user. To do this you have to make a POST request to the token endpoint and you need to know a client secret. Node code for doing that would look like following

var postData = querystring.stringify({
                'grant_type': 'authorization_code',
                'code': req.query.code,
                'client_id': this.clientId,
                'client_secret': this.clientSecret,
                'resource': this.clientId,
                'redirect_uri': this.callbackUrl,
                'scope': 'user.read'
            });

var tokenRequestOptions = {
                hostname: this.tokenUrl.hostname,
                port: 443,
                path: this.tokenUrl.pathname,
                method: 'POST',
                timeout: 45,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Content-Length': Buffer.byteLength(postData)
                }              				
            }
            
const tokenRequest = https.request(tokenRequestOptions, tokenResponse => {

A successful response has following fields in it and is returned as a string that you will have to parse using JSON.parse:

From here on, you can use either the access_token or the id_token to get user details. I found both have the same info. I used the id_token which is JWT token. To decode it, use the jwt-decode or jsonwebtoken library. The decoded token has following info in it

Decoded ID Token

The aud field is equal to your client ID and the UUID in iss field is equal to the issuer – the tenant ID. You can use this to validate the token. See this for what the other fields mean.

That’s it! You have authenticated the user! Observe that the client secret is never shared with the browser. The story does not end here because now you should set some persistent info – think cookie – in the response sent back to the browser so that when user visits the site again, they don’t have to log in again! Also, we need to redirect the browser to the page the user has been trying to visit all this time.

There are many ways you can set a cookie. I used following code where we use the jsonwebtoken library.

const userToken = {
            uid: userInfo.unique_name,
            name: userInfo.name
        };
        const signedToken = jwt.sign(userToken, this._jwtSecret, { expiresIn: 86400 }); // 24h
        const redirectUrl = url.resolve(this._siteUrl, landingUrl);
        res.status(200)
            .cookie(this._loginCookie, signedToken, { maxAge: 86400, httpOnly: true, secure: true })
            .redirect(redirectUrl);   

The secure=true option tells to transmit the cookie only via TLS and httpOnly=true prevents user from accessing the cookie via client-side javascript. Both are pretty important.

Now when user makes a request back again, s/he can be authenticated by examining the cookie. We use the cookie-parser library to access cookies using req.cookies in the code below.

const token = req && req.cookies && req.cookies[this._loginCookie];
        if (!token) {
            return false;
        }
        
        // Test the validity of the token
        try {
            const decodedToken = jwt.verify(token, this._jwtSecret);
            // Compare the token expiry (in seconds) to the current time (in milliseconds)
            // Bail out if the token has expired
            if (decodedToken.exp <= Date.now() / 1000) {
                return clearTokenAndNext();
            }            
            return {
                'email': decodedToken.uid,
                'name': decodedToken.name
            } 
        } catch (err) {
            return clearTokenAndNext();
        }

Finally we can wrap this up by adding a method that will logout a signed in user

logout(req, res) {
        this._clearLoginCookie(res);
        res.status(200).send('You have successfully logged out');
    }

    _clearLoginCookie(res) {
        res.clearCookie(this._loginCookie);        
    }

There you have it! With a little effort, it can all be done without using any library. You will need following dependencies:

"dependencies": {
    "cookie-parser": "~1.4.5",
    "express": "~4.17.1",
    "jwt-decode": "~2.2.0",
    "jsonwebtoken": "~8.5.1"
  }

Good Luck! Make it happen!

Implicit Grant explained: The best practice is to obtain the access or ID token on the server in the callback. But if you are developing a SPA which has no server-side code, then you have no choice but to obtain the token on the client side. To do this, you need to check the implicit grant boxes.

Again do this only if you have no server side code i.e., there is no choice but to get the token on client itself. https://tools.ietf.org/html/rfc6749#section-10.3 clearly states:

When using the implicit grant type, the access token is transmitted in the URI fragment, which can expose it to unauthorized parties.

If you enable implicit grant, you can acquire the access or ID token in the call to authorize itself as shown in https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow

For this to work you need to use response_type=id_token instead of response_type=code when making request to the authorize endpoint. The implicit grant allows you to get the token directly from the authorize endpoint without having to know the client secret. It short-circuits the step of using the access code to get the access or ID token in the callback on the server. TL;DR: don’t do it.

V1 vs V2: Azure AD supports two endpoints for OAuth. What’s the difference?
https://nicolgit.github.io/AzureAD-Endopoint-V1-vs-V2-comparison/
https://docs.microsoft.com/en-us/azure/active-directory/azuread-dev/azure-ad-endpoint-comparison#openid-profile-and-email
scopes are only supported by the v2 endpoint. In my case the v1 endpoint was perfect as it returns all the information about the user. See https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow and https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent for information on scopes.

  • The email scope allows your app access to the user’s primary email address through the email claim in the id_token, assuming the user has an addressable email address.
  • The profile scope affords your app access to all other basic information about the user, such as their name, preferred username, object ID, and so on, in the id_token.
This entry was posted in Software and tagged . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s