DEV Community

Tobias Urban
Tobias Urban

Posted on • Edited on

The (hands-on) beginners guide to Azure Active Directory

This article aims to give you a reference implementation of how you can log in your users using their existing Azure Active Directory accounts. Are you curious what Azure Active Directory (AAD) is in the first place? Then check out this article that walks you through the theoretical foundations of AAD.

You can find the final code of this tutorial here.

What we're building

Azure Active Directory is built on industry-standard identity specifications and supports Open ID Connect for modern authentication and authorization. This tutorial will walk you through the OAuth 2.0 authorization code flow, which consists of two basics steps:

  • First, you have to redirect your user to a Microsoft hosted login page. Microsoft takes care of receiving, handling and validating user credentials, so you don't have to care about any of that.
  • In the second step, Azure Active Directory will send an authorization code to our web server. We take this code and trade it for a JWT token, which then can be used to authorize calls to other Microsoft services like Microsoft Graph, AAD and more.

Getting started

As a prerequisite for this tutorial we will need some kind of server environment to run our code, in this case we chose Node.js. The first thing to do is to set up the server:

const express = require("express");
const app = express();
const request = require("request");
require("dotenv").config();
//Here goes our code
app.listen(8080);

In our first step we have to log in our user and receive a code string that we can trade in for a Bearer token. This tutorial assumes that you have already registered an app in Azure Active Directory, a manual for that can be found here. To get the code we have to redirect our user to the Microsoft login page (where they then log in with their credentials).

app.get("/login", (req, res) => {
res.redirect(
"https://login.microsoftonline.com/" + process.env.TENANT_ID + "/oauth2/v2.0/authorize?" +
"client_id=" + process.env.CLIENT_ID +
"&response_type=code" +
"&redirect_uri=" + process.env.BASE_URL + process.env.REDIRECT_URL +
"&response_mode=query" +
"&state= " +
"&scope=" + process.env.SCOPE
);
})

There are a lot of parameters in this URL, so let's walk through them one by one:

  • The base url: We need to call https://login.microsoftonline.com/*tenant*/oauth2/v2.0/authorize to enter our user data. The tenant can be either only our own tenant (e.g. myTenant.onmicrosoft.com) or common, which means every account that is part of some Active Directory tenant (e.g. thatOtherTenant.onmicrosoft.com) can log into our app. We further specify that we want to use version 2.0 of the REST API (you can find the differences between v1.0 and v2.0 here).

  • client_id: The id of your app that is registered in your AAD tenant

  • response_type: Specifies what Active Directory will return after a successful login action. For the OAuth 2.0 flow we always use code to receive the code we can trade for a Bearer token, in the OpenIdConnect flow we would have to use the value id_token.

  • redirect_uri: The url to which AAD will redirect the user when they successfully logged in. This url has to be specified in the app registration and your parameter must match this specified url exactly (including the path, so if you specify www.myapp.com as a redirect url in Active Directory and www.myapp.com/callback in your query parameters, the authentication will fail).

  • response_mode: This parameter determines how our access code will be sent back to our app. It can have the values query, form_post and fragment. In our case where we actually redirect the user we can use the query parameter, if we would for example open a pop-up for the user we could also use the form_post to post the code to our app.

  • state: Here you can enter any string you wish. This can be used as a challenge for security reasons or if you want to store some information the user gives to you at the beginning of the authentication flow and that should be given back at the end of the flow.

  • scope: The scopes are the permissions your users will have when they want to call any Microsoft services with the bearer token they receive. You can read more about scopes here.

Furthermore there are optional parameters that you can use (if applicable):

  • prompt: Here you can specify what the user will see when they try to log into your app. By default, Active Directory will sign your users in, store a cookie on their device and with every further authorization, it will log the user in silently without showing an additional prompt. With the values login, consent and none you can specify that the user either always sees the login form, that they always have to give consent to the app to use their data before using it or that they never see any form.
  • login_hint: If you already know the email address or user name that your user will take to log in, you can pre-fill this input field in the login mask with the login_hint parameter.
  • domain_hint: As already stated the Azure AD v2.0 endpoint supports login from consumers (private accounts) and organizations likewise. With this parameter you can specify if only organizations or only consumers can log into your app by giving it the value consumers or organizations.

When our user now logs into the application successfully, our redirect_uri will be called in the format redirect_uri?code=… . We can now listen to that path from our server and directly use the code to trade in for a token. Therefore we have to use a POST call to Azure AD, and we have to implement functionality on what to do with the token when we received it.

app.get(process.env.REDIRECT_URL, (req, res) => {
const authCode = req.query.code;
if (!authCode) {
res.status(500).send("There was no authorization code provided in the query. No Bearer token can be requested");
return;
}
const options = {
method: "POST",
url: "https://login.microsoftonline.com/" + process.env.TENANT_ID + "/oauth2/v2.0/token",
form: {
grant_type: "authorization_code",
code: authCode,
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET,
redirect_uri: process.env.BASE_URL + process.env.REDIRECT_URL
}
};
request(options, function (error, response, body) {
if (error) throw new Error(error);
try {
const json = JSON.parse(body);
if (json.error) res.status(500).send("Error occured: " + json.error + "\n" + json.error_description);
else {
res.send(json.access_token);
}
}
catch (e) {
res.status(500).send("The token acquirement did not return a JSON. Instead: \n" + body);
}
});
});

The POST call again has a few mandatory parameters that we have to provide to get the Bearer token.

  • url: Similar to the first call we can use either our tenant or the common endpoint and we can specify to use v1.0 or v2.0. The url has to match the url from the first call.
  • grant_type: Used to specify which kind of grant should be given back, must be authorization_code to get a Bearer token. You can look up the different grants here.
  • code: Our access code we got from our previous call.
  • client_id: The ID of our app registered in Azure Active Directory.
  • client_secret: A secret given out by Azure Active Directory used to prove that you are the developer of the app.
  • redirect_uri: Usually just the current url, as this is the redirect_uri you have specified in your last call and in AAD.

After we executed or POST call, we get an answer from Active Directory containing a JSON object that has the Bearer token in its parameters. If an error occurred the JSON object has an error attribute where you will receive an error message.

Now you have everything to authenticate your users and start working with their Microsoft profiles to build apps based on the Microsoft ecosystem. For example, if you gave your app the scope user.read, you can now do a GET request to Microsoft graph to get the users profile information.

app.get("/me", (req,res) => {
const options = {
method: "GET",
url: "https://graph.microsoft.com/v1.0/me/",
header: {
Authorization: "Bearer *your-token*"
}
}
request(options, function (error, response, body) {
res.send(body);
})
})

You can find the whole code (documented) here.

Top comments (0)