Preparing to create the app using programmable authentication flow

In the case of creating RPA or an app executes function automatically, the article ‘Microsoft Graph API provides several grant types of The OAuth 2.0 Authorization Framework’ describes that the ‘password’ and the ‘client_credentials’ grant type authentication flow is useful.
The ‘password’ grant type authentication flow can’t use the Microsoft Account, because the ‘client_credentials’ grant type needs credential which is authorized by a specific domain. Microsoft Account has not an individual domain that provides a credential for the account.
Although, there is the ‘Microsoft 365 Developer Program‘ for developers, so the article ‘Programmable authentication flow for accessing to Microsoft Graph’ describes the ‘password’ grant type authentication flow with this program which provides the Organization Account. This article descrives preparing for the article.
It is important that the ‘password’ grant type is supported on the ‘v1.0’ version of the the Azure Active Directory endpoint. In case of creating an app using the Microsoft Graph API, the Azure Active Directory endpoint ‘v2.0’ version is better than using the ‘v1.0’ version.
This article using the code sample of the previous article which already includes the feature of acquiring user information using the token from the ‘authorization code’ grant type authentication flow.

>>The code sample of this article

・Story of programmable auth flow

For describing programmable authentication flow, the scenario of this article show below cases as a sample.

1. The app has a login button.

2. The app navigates to the action that requests authorization to the Azure Active Directory endpoint when a user presses the login button.

3. The app acquires a token as the ‘user.read’ permission from the Azure Active Directory endpoint. And use it, the app requests user information to the Microsoft Graph API.

4. The app acquires a token as the ‘file.read’ permission from the Azure Active Directory endpoint. And use it, the app requests a share link of the OneDrive item from the Microsoft Graph API.

This article describes until 3rd part of this scenario for preparing the app that controls tokens that have minimum permission per the feature.

・Set Azure Active Directory application

Regarding how to create the Azure Active Directory application is refer to the article. Describe how to set the application permission here.

Go to the Azure portal, and select the Azure Active Directory, continue select target an app. Then select the ‘API permission’ section and the ‘Add a permission’, the ‘Microsoft Graph’, the ‘Delegated permissions’, select the ‘File.ReadWrite’ after put file to input box of the ‘Select permissions’ section. Press the ‘Add permissions’ button at last.

・Setting configuration for development environment

As first, download the code sample, restore packages and run debug done, then modify ‘Sign In’ action in the ‘Account’ controller of [Controllers] folder at the Solution Explorer to usable for the development environment.

using Microsoft.AspNetCore.Mvc;

namespace React.Sample.Webpack.CoreMvc.Controllers
{
  public class AccountController : Controller
    {
    [Route("Account/SignIn")]
    [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
    public void SignIn()
    {
      var tenant = "__YOUR TENANT__";//this grant_type allows common
      var clientId = "__APP CLIENT ID__";
      var redirectUri = "http://localhost:9457/home";
      Response.Redirect("https://login.microsoftonline.com/" + tenant 
        + "/oauth2/v2.0/authorize?client_id=" + clientId + "&redirect_uri=" 
        + redirectUri + "&grant_type=implicit&response_type=code&scope=User.Read");
    }
  }
}

Modify lines of the variable ‘tenant’ and ‘clientId’ to useable for the development environment(11th line and 12th line).

And open ‘Index’ action in the ‘Home’ controller of [Controllers] folder at the Solution Explorer, put a breakpoint at the 45th line. Then run debug and confirm acquiring ‘code’ and ‘state’ variables.

・Share user resource to another user

As a sample resource, create an Excel file to OneDrive. it is good which creating an Excel file from Microsoft Teams or OneDrive as without Microsoft Excel or downloading from Microsoft Office.com.

It has to use account same the OneDrive log in and ‘password’ grant type flow.

At first, Go to Microsoft Graph Explorer to take the file ID of the OneDrive Driveitem. Sign-in with the OneDrive account, add the ‘OneDrive’ sample category using ‘show more samples’ of the end of the ‘Sample Queries’.

Select the ‘search my OneDrive’ GET method and exchange the ‘finance’ to the sample Excel file name at the ‘Run Query’ input box. Then press the ‘Run Query’ button.
When the response has taken, take the memo the ‘id’ property in the ‘value’ property of the response.

As continue, change the Method box to the POST method and put code below to the ‘Run Query’ input box, set code below to the ‘Request Body’ and check the ‘Request Header’ is the same as figure below.

Then press the ‘Run Query’ button and confirm to be able to acquire the ‘webUrl’ property in the ‘link’ property of the response.

the 'Run Query' input box :
https://graph.microsoft.com/v1.0/me/drive/items/__FILE ID OF MEMO__/createLink

the 'Request Body' :
{"type": "edit", "scope": "anonymous"}

Creating the shared link refers to the document of the Mictrosoft Docs.

・Modify the code sample to manage minimum scope per individual access

Move four rows in the ‘if’ condition that the code was able to acquire, to out of the condition(52nd row to 55th row of the figure below).

Create the ‘ApplicationUser’ class(use at 41st row of figure below) as a container of login user information instead of the ‘accountName’ variable(49th row of the figure below). Comment out the ‘accountName’ variable.

Continue, to fix the error, change all of the ‘accountName’ variable to the propertie ‘GlobalName’ of the ‘user’ variable.

The ‘user’ variable has information of connect to Microsoft Graph that has set at the ‘AccountConoroller’ when befor trancesion to this contoroller. The ‘ApplicationUser’ object has transfered as seriarized text. When accept the object, deserialize it to use.

When an authorization code acquire, the ‘client_id’ parameter has not set, so it add to the ‘user’ variable. And the line is execute in the ‘if’ condition that the app can acquire the code, so put the line into the ‘if’ condition(figure below is before move it). And the ‘GlobalName’ property of the ‘user’ object also input after the ‘if’ condition that the ‘user.GlobalName’ is not empty and not null, so it will move correct position but it refer to folow description.

if (!string.IsNullOrEmpty(code))
{
  user.AccessList.LastOrDefault().Secret = "__CLIENT SECRET__";
  user.AccessList.LastOrDefault().AuthCode = code;
  user.AccessList.LastOrDefault().AADEndPoint = "v2.0";
  ...
  if (signIn)
  {
    ...
    ViewBag.AccountName = user.GlobalName;
  }
}

The ‘ApplicationUser’ class as a container of login user information instead of the ‘accountName’ variable.(48th row to 49th row of the figure below). Then Change all of the ‘accountName’ variable to the propertie ‘GlobalName’ of the ‘account’ variable.

//This Class is to add custom properties for the app 
public class ApplicationUser : MSGraphUser 
{
    // to select AAD endpoint
    public bool IsAADUser { get; set; }
    // to manage minimum scope per individual access
    public List<AccessHistory> AccessList { get; set; }
    // for not single bite region
    public string GlobalName { get; set; }
}
public class AccessHistory
{
    // to select AAD user or Live ID
    public string AADEndPoint { get; set; }
    public string TenantID { get; set; }
    public string ClientId { get; set; }
    public string Secret { get; set; }
    public Uri Redirect { get; set; }
    public string AuthCode { get; set; }
    public string Resource { get; set; }
    public string Scope { get; set; }
    public string GrantType { get; set; }
    public string ResponseType { get; set; }
    // to connect token and how to acquired
    public MSGraphAuthTokens AuthTokens { get; set; }
}

The AccountController has to provides the ‘ApplicationUser’ object to the ‘Index’ Action of HomeController. The AccountController trancesion to Azure Active Directory Endpoint before the HomeController, so the object has to be serialized and put into TempData of ASP.NET.

public class AccountController : Controller
{
  [Route("Account/SignIn")]
  [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
  public void SignIn()
  {
    var user = new ApplicationUser();
    user.AccessList = new List<AccessHistory>();
    var history = new AccessHistory();
    //var tenant = "__YOUR TENANT__";
    history.TenantID = "__YOUR TENANT__";
    //var clientId = "__APP CLIENT ID__";
    history.ClientId = "__APP CLIENT ID__";
    //var redirectUri = "http://localhost:9457/home";
    history.Redirect = new Uri("http://localhost:9457/home");
    history.GrantType = "implicit";
    history.ResponseType = "code";
    history.Scope = "User.Read";
    history.AADEndPoint= "v2.0";
    user.AccessList.Add(history);
    TempData["User"] = JsonConvert.SerializeObject(user);
    Response.Redirect("https://login.microsoftonline.com/"
      + user.AccessList.LastOrDefault().TenantID + "/oauth2/" + history.AADEndPoint
      + "/authorize?client_id=" + user.AccessList.LastOrDefault().ClientId
      + "&redirect_uri=" + user.AccessList.LastOrDefault().Redirect
      + "&grant_type=" + user.AccessList.LastOrDefault().GrantType
      + "&response_type=" + user.AccessList.LastOrDefault().ResponseType
      + "&scope=" + user.AccessList.LastOrDefault().Scope);
  }
}

・Override the GetToken method

Copy the ‘GetToken’ method and paste above the ‘GetToken’ method, then change return value from string to bool and arguments to ‘ApplicationUser user’. Errors to fix like as figure below.

The part of creating Url change to code below.

//var url = $"https://login.microsoftonline.com/" + user.AccessList.LastOrDefault().TenantID + "/oauth2/v2.0/token";
//url = $"https://login.microsoftonline.com/common/oauth2/token";// live API
var url
  = $"https://login.microsoftonline.com/" + user.AccessList.LastOrDefault().TenantID + "/oauth2";
if (user.AccessList.LastOrDefault().GrantType=="password" || !user.IsAADUser 
  || user.AccessList.LastOrDefault().GraphVersion=="v1.0" 
  || string.IsNullOrEmpty(user.AccessList.LastOrDefault().GraphVersion))
{
  url += "/token";// live API
}
else
{
  url += "/" + user.AccessList.LastOrDefault().GraphVersion + "/token";// AAD
}

・Set user information

The ‘ApplicationUser’ class has a container of information of Azure Active Directory account. At next, create the ‘SetUserInfo’ method in the ‘AccessGraph’ class.

public void SetUserInfo(ApplicationUser account)
{
  var token = account.AccessList
            .Where(t => Regex.IsMatch(t.Scope, @"user\.read",RegexOptions.IgnoreCase))
            .FirstOrDefault();
  if (token == null) return;
  MSGraphUser user = null;
  var url = $"https://graph.microsoft.com/v1.0/me/";
  using (var httpClient = new HttpClient())
  {
    httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + token.AuthTokens.access_token);
    var res = httpClient.GetAsync(url).Result;
    string resultJson = res.Content.ReadAsStringAsync().Result;
    if (res.IsSuccessStatusCode)
    {
      user = JsonConvert.DeserializeObject<MSGraphUser>(resultJson);
      account.businessPhones = user.businessPhones;
      account.displayName = user.displayName;
      account.givenName = user.givenName;
      account.id = user.id;
      account.jobTitle = user.jobTitle;
      account.mail = user.mail;
      account.mobilePhone = user.mobilePhone;
      account.officeLocation = user.officeLocation;
      account.preferredLanguage = user.preferredLanguage;
      account.surname = user.surname;
      account.userPrincipalName = user.userPrincipalName;
    }
  }
  return;
}

In the ‘Index’ action of the ‘HomeController’, modify the part of’!string.IsNullOrEmpty(code)’ of the ‘if’ condition as like below code.

if (!string.IsNullOrEmpty(code))
{
  token = new AccessGraph().GetToken(user.AccessList.LastOrDefault().ClientId, user.AccessList.LastOrDefault().Secret, user.AccessList.LastOrDefault().Redirect.AbsoluteUri, "", code, user.AccessList.LastOrDefault().TenantID, "user.read");
  if (!string.IsNullOrEmpty(token)) signIn = true;
}
//if (signIn) accountName = new AccessGraph().GetUser(token);
if (signIn) user.GlobalName = new AccessGraph().GetUser(token);
if (!string.IsNullOrEmpty(user.GlobalName)) 
{
  //Regist '髙尾 哲朗(Tetsuro Takao)' as Azure Active Directory account.
  user.GlobalName = string.Join("", Regex.Matches(user.displayName, @"[a-z | A-Z]*")).Trim();
}

//to

if (!string.IsNullOrEmpty(code))
{
  token = new AccessGraph().GetToken(user.AccessList.LastOrDefault().ClientId, user.AccessList.LastOrDefault().Secret, user.AccessList.LastOrDefault().Redirect.AbsoluteUri, "", code, user.AccessList.LastOrDefault().TenantID, "user.read");
  if (!string.IsNullOrEmpty(token)) signIn = true;
}
if (signIn) user.GlobalName = new AccessGraph().GetUser(token);
if (signIn) 
{
  //Regist '髙尾 哲朗(Tetsuro Takao)' as Azure Active Directory account.
  user.GlobalName = string.Join("", Regex.Matches(user.displayName, @"[a-z | A-Z]*")).Trim();
}

//to

if (!string.IsNullOrEmpty(code))
{
  token = new AccessGraph().GetToken(user.AccessList.LastOrDefault().ClientId, user.AccessList.LastOrDefault().Secret, user.AccessList.LastOrDefault().Redirect.AbsoluteUri, "", code, user.AccessList.LastOrDefault().TenantID, "user.read");
  if (!string.IsNullOrEmpty(token)) signIn = true;
}
if (signIn) 
{
  user.GlobalName = new AccessGraph().GetUser(token);
  //Regist '髙尾 哲朗(Tetsuro Takao)' as Azure Active Directory account.
  user.GlobalName = string.Join("", Regex.Matches(user.displayName, @"[a-z | A-Z]*")).Trim();
}

//to

if (!string.IsNullOrEmpty(code))
{
  token = new AccessGraph().GetToken(user.AccessList.LastOrDefault().ClientId, user.AccessList.LastOrDefault().Secret, user.AccessList.LastOrDefault().Redirect.AbsoluteUri, "", code, user.AccessList.LastOrDefault().TenantID, "user.read");
  if (!string.IsNullOrEmpty(token)) signIn = true;
  if (signIn) 
  {
    user.GlobalName = new AccessGraph().GetUser(token);
    //Regist '髙尾 哲朗(Tetsuro Takao)' as Azure Active Directory account.
    user.GlobalName = string.Join("", Regex.Matches(user.displayName, @"[a-z | A-Z]*")).Trim();
  }
}

//to

if (!string.IsNullOrEmpty(code))
{
  token = new AccessGraph().GetToken(user.AccessList.LastOrDefault().ClientId, user.AccessList.LastOrDefault().Secret, user.AccessList.LastOrDefault().Redirect.AbsoluteUri, "", code, user.AccessList.LastOrDefault().TenantID, "user.read");
  if (!string.IsNullOrEmpty(token)) signIn = true;
  if (signIn) 
  {
    new AccessGraph().SetUserInfo(user);
    //Regist '髙尾 哲朗(Tetsuro Takao)' as Azure Active Directory account.
    user.GlobalName = string.Join("", Regex.Matches(user.displayName, @"[a-z | A-Z]*")).Trim();
  }
}

//to

if (!string.IsNullOrEmpty(code))
{
  user.AccessList.LastOrDefault().Secret = "__CLIENT SECRET__";
  user.AccessList.LastOrDefault().AuthCode = code;
  user.AccessList.LastOrDefault().AADEndPoint = "v2.0";
  signIn = new AccessGraph().GetToken(user);
  if (signIn)
  {
    new AccessGraph().SetUserInfo(user);
    //Regist '髙尾 哲朗(Tetsuro Takao)' as Azure Active Directory account.
    user.GlobalName = string.Join("", Regex.Matches(user.displayName, @"[a-z | A-Z]*")).Trim();
    ViewBag.AccountName = user.GlobalName;
  }
}

・Change the way to set the POST properties

The ‘GetToken’ method in the ‘AccessGraph’ class argues the variable as the ‘ApplicationUser’ class. The object has the ‘AccessList’ property that includes all properties of required value to acquire token. Modify request to use it.

using (var httpClient = new HttpClient())
{
    var properties = "client_id=" + user.AccessList.LastOrDefault().ClientId + "&client_secret=" + user.AccessList.LastOrDefault().Secret + "&scope=" + user.AccessList.LastOrDefault().Scope + "&redirect_uri=" + user.AccessList.LastOrDefault().Redirect;
    if (!string.IsNullOrEmpty(user.AccessList.LastOrDefault().AuthTokens.refresh_token))
    {
        properties += "&refresh_token=" + user.AccessList.LastOrDefault().AuthTokens.refresh_token + "&grant_type=refresh_token";
    }
    if (!string.IsNullOrEmpty(user.AccessList.LastOrDefault().AuthCode))
    {
        properties += "&code=" + user.AccessList.LastOrDefault().AuthCode + "&grant_type=authorization_code";
    }
    var content = new StringContent(properties, Encoding.UTF8, "application/x-www-form-urlencoded");
    var res = httpClient.PostAsync(url, content).Result;
    string resultJson = res.Content.ReadAsStringAsync().Result;
    if (res.IsSuccessStatusCode)
    {
        user.AccessList.LastOrDefault().AuthTokens = JsonConvert.DeserializeObject<MSGraphAuthTokens>(resultJson);
        result = true;
    }
}

//to

using (var httpClient = new HttpClient())
{
  var properties 
        = "client_id=" + user.AccessList.LastOrDefault().ClientId + "&client_secret=" + user.AccessList.LastOrDefault().Secret;
  if (!string.IsNullOrEmpty(user.AccessList.LastOrDefault().Redirect.AbsoluteUri))
  {
    properties += "&redirect_uri=" + user.AccessList.LastOrDefault().Redirect.AbsoluteUri;
  }
  if (user.AccessList.LastOrDefault().GrantType.Contains("password"))
  {
    var account = JsonConvert.DeserializeObject<JObject>&kt(user.AccessList.LastOrDefault().GrantType);
    properties += "&grant_type=password&username=" + account["username"];
    properties += "&password=" + account["password"];
    properties += "&resource=https://graph.microsoft.com";
  }
  else
  {
    properties += "&scope=" + user.AccessList.LastOrDefault().Scope;
  }
  if (user.AccessList.LastOrDefault().GrantType == "refresh")
  {
    properties += "&refresh_token=" + user.AccessList.LastOrDefault().AuthTokens.refresh_token + "&grant_type=refresh_token";
  }
  if (user.AccessList.LastOrDefault().GrantType == "client_credentials") properties += "&grant_type=client_credentials";
  if (!string.IsNullOrEmpty(user.AccessList.LastOrDefault().AuthCode))
  {
    properties += "&code=" + user.AccessList.LastOrDefault().AuthCode + "&grant_type=authorization_code";
  }
  var content = new StringContent(properties, Encoding.UTF8, "application/x-www-form-urlencoded");
  var res = httpClient.PostAsync(url, content).Result;
  string resultJson = res.Content.ReadAsStringAsync().Result;
  if (res.IsSuccessStatusCode)
  {
    user.AccessList.LastOrDefault().AuthTokens = JsonConvert.DeserializeObject<MSGraphAuthTokens>(resultJson);
    result = true;
  }
}

>>The code sample of this article

About takao

I'm Microsoft MVP since June 2010.