[Azure] 透過Graph API,取得本地端AD同步至Azure AD上的使用者帳號資訊及其工作資訊

在前篇文章[Azure] 同步本地端AD與Azure AD的帳號與群組資訊中,說明了如何透過Azure AD Connect將本地端的AD資料與Azure AD進行同步
本篇文章則會將同步至Azure AD上的使用者帳號資訊,透過程式的方式呼叫微軟提供的Graph API,取得使用者的帳號基本資訊、工作資訊,以及上層主管

在使用Graph API上,一共會使用到三個Graph API,分別是Get usersGet a user以及Get a user's manager

首先,先打開傳統的Azure管理介面,並進入Azure AD的內容,建立一個新的應用程式,應用程式類型選擇"WEB 應用程式和/或 WEB API"

接著下一步,給予登入URL與應用程式識別碼URI,這些值可以先隨意設定,稍後也可以進行修改

完成設定後,點選進入該應用程式的設定畫面,並將"用戶端識別碼"記起來,然後建立一組金鑰,當然金鑰值一樣先將它記下來

接著,確認一下本地端的AD是否有將資訊同步至Azure AD上,在這個範例裡,我建立了一個使用者帳號john,其主管為Bill,公司名稱與部門職稱等等的,都有設定進去

Azure AD上也已經同步完成,並保有本地端的工作資訊設定了

接著,打開Visual Studio,並建立一個Windows Form的專案,加入一個AzureADUtility.cs的類別庫,並將下面程式碼加入至該類別庫中

public class AzureADUtility
{
    public string Tenant { get; set; }
    public string ClientId { get; set; }
    public string Secret { get; set; }
    HttpStatusCode code;

    public AzureADUtility(string strTenant, string strClientId, string strSecret)
    {
        this.Tenant = strTenant;
        this.ClientId = strClientId;
        this.Secret = strSecret;
    }

    public Models.User.Result GetUsers()
    {
        string strUrl = "https://graph.windows.net/" + this.Tenant + "/users?api-version=1.6";
        Models.User.Result objResults = JsonConvert.DeserializeObject<Models.User.Result>(this.CallGraphAPI(strUrl, "GET", "", out code));
        return objResults;
    }

    public Models.User.Manager GetManager(string strObjectId)
    {
        string strUrl = $"https://graph.windows.net/{this.Tenant}/users/{strObjectId}/$links/manager?api-version=1.6";
        string strContent = this.CallGraphAPI(strUrl, "GET", "", out code);
        Models.User.Manager objMng = null;
        if (code == HttpStatusCode.OK)
            objMng = JsonConvert.DeserializeObject<Models.User.Manager>(strContent);
        return objMng;
    }

    protected string GetAuthorizationHeader()
    {
        AuthenticationResult result = null;
        var context = new AuthenticationContext("https://login.microsoftonline.com/" + this.Tenant);
        var thread = new Thread(() =>
        {
            result = context.AcquireToken("https://graph.windows.net", new ClientCredential(this.ClientId, this.Secret));
        });

        thread.SetApartmentState(ApartmentState.STA);
        thread.Name = "AquireTokenThread";
        thread.Start();
        thread.Join();

        if (result == null)
        {
            throw new InvalidOperationException("Failed to obtain the JWT token");
        }

        return result.AccessToken;
    }

    protected string CallGraphAPI(string strUrl, string strHttpMethod, string strPostContent, out HttpStatusCode code)
    {
        string token = GetAuthorizationHeader();
        HttpWebRequest request = HttpWebRequest.Create(strUrl) as HttpWebRequest;
        request.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + token);
        request.Method = strHttpMethod;
        code = HttpStatusCode.OK;

        if (strPostContent != "" && strPostContent != string.Empty)
        {
            request.KeepAlive = true;
            request.ContentType = "application/json";

            byte[] bs = Encoding.ASCII.GetBytes(strPostContent);
            Stream reqStream = request.GetRequestStream();
            reqStream.Write(bs, 0, bs.Length);
        }

        string strReturn = "";
        try
        {
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            var respStream = response.GetResponseStream();
            strReturn = new StreamReader(respStream).ReadToEnd();
        }
        catch (Exception e)
        {
            strReturn = e.Message;
            code = HttpStatusCode.NotFound;
        }

        return strReturn;
    }
}

這個類別庫的程式碼,主要是用來取得使用者帳號資料,以及上層主管資料用的程式碼,當然還包含了呼叫Graph API的部份

接著建立第二個類別庫Models\User.cs,並將下面的程式碼加入至該類別庫中

public class User
{
    public class Value
    {
        public string odatatype { get; set; }
        public string objectType { get; set; }
        public string objectId { get; set; }
        public object deletionTimestamp { get; set; }
        public bool accountEnabled { get; set; }
        public object[] signInNames { get; set; }
        public Assignedlicens[] assignedLicenses { get; set; }
        public Assignedplan[] assignedPlans { get; set; }
        public object city { get; set; }
        public string companyName { get; set; }
        public string country { get; set; }
        public object creationType { get; set; }
        public string department { get; set; }
        public bool? dirSyncEnabled { get; set; }
        public string displayName { get; set; }
        public object facsimileTelephoneNumber { get; set; }
        public string givenName { get; set; }
        public string immutableId { get; set; }
        public object isCompromised { get; set; }
        public string jobTitle { get; set; }
        public DateTime? lastDirSyncTime { get; set; }
        public string mail { get; set; }
        public string mailNickname { get; set; }
        public string mobile { get; set; }
        public string onPremisesSecurityIdentifier { get; set; }
        public string[] otherMails { get; set; }
        public string passwordPolicies { get; set; }
        public Passwordprofile passwordProfile { get; set; }
        public object physicalDeliveryOfficeName { get; set; }
        public object postalCode { get; set; }
        public string preferredLanguage { get; set; }
        public object[] provisionedPlans { get; set; }
        public object[] provisioningErrors { get; set; }
        public string[] proxyAddresses { get; set; }
        public DateTime refreshTokensValidFromDateTime { get; set; }
        public string sipProxyAddress { get; set; }
        public object state { get; set; }
        public object streetAddress { get; set; }
        public string surname { get; set; }
        public object telephoneNumber { get; set; }
        public string thumbnailPhotoodatamediaContentType { get; set; }
        public string usageLocation { get; set; }
        public string userPrincipalName { get; set; }
        public string userType { get; set; }
        public string ManagerUri { get; set; }
        public string ManagerObjectId { get; set; }
        public string Manager { get; set; }
    }

    public class Passwordprofile
    {
        public object password { get; set; }
        public bool forceChangePasswordNextLogin { get; set; }
        public bool enforceChangePasswordPolicy { get; set; }
    }

    public class Assignedlicens
    {
        public object[] disabledPlans { get; set; }
        public string skuId { get; set; }
    }

    public class Assignedplan
    {
        public DateTime assignedTimestamp { get; set; }
        public string capabilityStatus { get; set; }
        public string service { get; set; }
        public string servicePlanId { get; set; }
    }

    public class Result
    {
        public string odatametadata { get; set; }
        public Value[] value { get; set; }
    }

    public class Manager
    {
        public string odatametadata { get; set; }
        public string url { get; set; }
    }
}

這個類別庫主要目的是為了定義出從Graph API上取得資料的JSON資料格式,方便轉為物件讓我們使用

在拉置畫面的動作中,我們在Windows Form上放一個DataGrid以及三個文字欄位,分別是設定網域的Tenant、剛剛在Azure上記錄的"用戶端識別碼"與"金鑰",並加入兩個Button,分別是取得AD上的使用者,以及其主管的動作

接下來在Get AAD Users的動作中,呼叫AzureADUtility.cs裡的GetUsers()這個程式

User.Result objResult;
AzureADUtility objAAD;

public frmMain()
{
    InitializeComponent();
    objAAD = new AzureADUtility(txtTenant.Text, txtClientId.Text, txtSecret.Text);
}

private void btnGetAADUsers_Click(object sender, EventArgs e)
{
    objResult = objAAD.GetUsers();
    gvUsers.DataSource = objResult.value;
}

這段主要的目的在透過呼叫Graph API後,將取得的所有使用者帳號資料列在DataGrid上,從下圖就可以確認到,在這個網域中的帳號資料都已經被取出,並列在DataGrid之中,當然也包含了工作資訊,如行動電話等等的內容

接著,我們在Get Users Manager的按鈕動作上,加入下面的程式碼

private void btnGetManager_Click(object sender, EventArgs e)
{
    // 取出該帳號的ObjectId, 並透過GraphAPI取得主管的資訊
    for (int i=0; i<objResult.value.Length; i++)
    {
        string strObjectId = objResult.value[i].objectId.ToString();

        // 呼叫GraphAPI,取得主管的資訊
        User.Manager objManager = objAAD.GetManager(strObjectId);

        if (objManager != null)
        {
            // 取代回傳的Url,並取出主管的ObjectId
            string strManagerObjectId = objManager.url.Replace("https://graph.windows.net/" + txtTenant.Text + "/directoryObjects/", "");
            strManagerObjectId = strManagerObjectId.Replace("/Microsoft.DirectoryServices.User", "");

            // 找出主管的資料
            User.Value objUser = objResult.value.FirstOrDefault(x => x.objectId == strManagerObjectId);

            // 重新放入欄位
            objResult.value[i].ManagerUri = objManager.url;
            objResult.value[i].ManagerObjectId = strManagerObjectId;
            objResult.value[i].Manager = objUser.displayName;
        }
    }

    gvUsers.DataSource = objResult.value;
}

由於透過Graph API取得主管資訊的動作,並不是直接回傳該主管的資訊,而是回傳主管資訊的網頁連結,就如同Graph API的頁面上所提供的資訊一樣,所以必須在程式中將取得的url字串置換掉,保留該主管的Guid即可,因為有了Guid,就可以再透過Graph API找出該物件的基本資訊

最後可以看到結果,連主管的資訊都可以取得了

本地端的AD同步至Azure AD並進行單一登入的作法有越來越多公司開始採用這樣的運作模式,當然也會有越來越多的工作流程與組織內容會依賴現有的AD資訊,並同步至Azure上作更多的利用
透過Graph API的操作,可以更有效的利用Azure AD上取得資訊的介面,達到更大的運用彈性

範例程式已放上Github:https://github.com/madukapai/maduka-Azure-AD