using System; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using C4IT.Logging; namespace C4IT.MsGraph { public class cMsGraphBase { public enum eHttpMethod { get, post, put, delete}; public const string constAzureLoginUrl = "https://login.microsoftonline.com/{Tenant}/oauth2/v2.0/token"; public const string constAzureAuthorizeUrl = "https://login.microsoftonline.com/{Tenant}/oauth2/v2.0/authorize"; public const string constMsGraphUrl = "https://graph.microsoft.com/{Version}/{Request}"; public const string constMsGraphApplicationGet = "applications?$filter=appId eq '{AppID}'"; public const string constMsGraphProfileGet = "me"; public bool IsOnline { get; private set; } = false; public string AccessToken { get; private set; } = null; public DateTime TokenExpiresIn { get; private set; } = DateTime.MinValue; private cMsGraphLogonInfo privLogonInfo = null; public Exception LastException { get; private set; } = null; public string LastErrorMessage { get; private set; } = null; public cMsGraphResultApplication Me { get; private set; } = null; public HttpStatusCode? LastHttpErrorCode { get; private set; } = null; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResetError() { LastException = null; LastErrorMessage = null; LastHttpErrorCode = null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetErrorException(string Action, Exception E, LogLevels lev = LogLevels.Error) { LastException = E; LastErrorMessage = Action + ": " + E.Message; cLogManager.LogEntry(Action, lev); } private async Task privLogonAsync(cMsGraphLogonInfo LogonInfo) { try { ResetError(); IsOnline = false; // prepare the http login request var postData = new List> { new KeyValuePair("client_id", LogonInfo.ClientID), new KeyValuePair("scope", LogonInfo.Scope), new KeyValuePair("redirect_uri", $"https://login.microsoftonline.com/{LogonInfo.Tenant}/oauth2/nativeclient"), new KeyValuePair("grant_type", "client_credentials"), new KeyValuePair("client_secret", LogonInfo.ClientSecret) }; var formData = new FormUrlEncodedContent(postData); var strUrl = constAzureLoginUrl.Replace("{Tenant}", LogonInfo.Tenant); var request = new HttpRequestMessage(System.Net.Http.HttpMethod.Post, strUrl) { Content = formData }; // do the http login request using (var httpClient = new HttpClient()) { var response = await httpClient.SendAsync(request); LastHttpErrorCode = response.StatusCode; if (!response.IsSuccessStatusCode) { LastErrorMessage = $"HTTP request error {response.StatusCode} while Azure tenant login: {response.ReasonPhrase}"; return false; } // get the response content & the access token var content = await response.Content.ReadAsStringAsync(); dynamic iToken = JsonConvert.DeserializeObject(content); AccessToken = iToken.access_token; int ExpiresIn = iToken.expires_in; TokenExpiresIn = DateTime.UtcNow + TimeSpan.FromSeconds(ExpiresIn * 0.7); var TokenType = iToken.token_type; if (TokenType != "Bearer") return false; } if (AccessToken != null) { IsOnline = true; Me = await RequestApplicationAsync(LogonInfo.ClientID); return true; } } catch (Exception E) { SetErrorException("HTTP exception error while Azure tenant login", E, LogLevels.Debug); cLogManager.LogException(E, LogLevels.Debug); } return false; } private async Task privRelogon() { if (DateTime.UtcNow < TokenExpiresIn) return true; if (privLogonInfo == null) return false; var RetVal = await privLogonAsync(privLogonInfo); return RetVal; } public async Task LogonAsync(cMsGraphLogonInfo LogonInfo) { var RetVal = await privLogonAsync(LogonInfo); if (RetVal == true) privLogonInfo = LogonInfo; return RetVal; } private async Task privRequestAsync(string Url, eHttpMethod httpMethod = eHttpMethod.get, object JsonData = null, bool retryForbidden = false) { ResetError(); try { await RequestCriticalSection.Semaphore.WaitAsync(); if (!IsOnline) return null; if (!await privRelogon()) return null; var retryCount = 0; while (true) { // create the request using (var handler = new HttpClientHandler()) { using (var client = new HttpClient(handler)) { client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken); HttpResponseMessage response; StringContent CO = null; if (JsonData != null) { var strJson = JsonConvert.SerializeObject(JsonData); CO = new StringContent(strJson, Encoding.UTF8, "application/json"); } switch (httpMethod) { case eHttpMethod.get: response = await client.GetAsync(Url); break; case eHttpMethod.post: response = await client.PostAsync(Url, CO); break; case eHttpMethod.delete: response = await client.DeleteAsync(Url); break; case eHttpMethod.put: response = await client.PutAsync(Url, CO); break; default: return null; } LastHttpErrorCode = response.StatusCode; if (!response.IsSuccessStatusCode) { if (retryForbidden && (LastHttpErrorCode == HttpStatusCode.Forbidden) && (retryCount < 2)) { retryCount++; await (Task.Delay(100)); } else { LastErrorMessage = $"HTTP request error {(int)response.StatusCode} while MS Graph request '{Url}': {response.ReasonPhrase}"; if (cLogManager.DefaultLogger.IsDebug) cLogManager.DefaultLogger.LogEntry(LogLevels.Warning, LastErrorMessage); return null; } } else { if (response.StatusCode == HttpStatusCode.NoContent) return true; var content = await response.Content.ReadAsStringAsync(); dynamic jsonData = JsonConvert.DeserializeObject(content); ; return jsonData; } } } } } catch (Exception E) { SetErrorException("HTTP exception error while while MS Graph request '{Request}'", E); cLogManager.LogException(E); } finally { RequestCriticalSection.Semaphore.Release(); } return null; } public async Task RequestAsync(string Request, bool UseBeta = false, eHttpMethod httpMethod = eHttpMethod.get, object JsonData = null, bool retryForbidden = false) { try { // get the request url var strVersion = "v1.0"; if (UseBeta) strVersion = "beta"; var strUrl = constMsGraphUrl.Replace("{Version}", strVersion); strUrl = strUrl.Replace("{Request}", Request); var data = await privRequestAsync(strUrl, httpMethod, JsonData, retryForbidden); if (data == null) return null; if (data is bool isValid) { if (isValid) return new cMsGraphResultBase(null); else return null; } var RetVal = new cMsGraphResultBase(data); return RetVal; } catch (Exception E) { cLogManager.LogException(E); } return null; } public async Task RequestListAsync(string Request, bool UseBeta = false, bool loadPaged = false, bool retryForbidden = false) { try { // get the request url var strVersion = "v1.0"; if (UseBeta) strVersion = "beta"; var strUrl = constMsGraphUrl.Replace("{Version}", strVersion); strUrl = strUrl.Replace("{Request}", Request); var res = await privRequestAsync(strUrl, retryForbidden: retryForbidden); if (res != null) { var RetVal = new cMsGraphResultList(retryForbidden); RetVal.AddResult(res); if (!RetVal.hasRemaining || loadPaged) return RetVal; while (RetVal.hasRemaining) { if (!await RequestNextAsync(RetVal)) return RetVal; } return RetVal; } } catch (Exception E) { cLogManager.DefaultLogger.LogException(E); } return null; } public async Task RequestNextAsync(cMsGraphResultList List) { try { var res = await privRequestAsync(List.NextResultUrl, retryForbidden: List.retryForbidden); if (res != null) { List.AddResult(res); return true; } } catch (Exception E) { cLogManager.DefaultLogger.LogException(E); } return false; } public async Task RequestProfileAsync() { ResetError(); try { var Result = await RequestAsync(constMsGraphProfileGet); if (Result != null) return new cMsGraphResultUser(Result); } catch (Exception E) { cLogManager.LogException(E); } return null; } public async Task RequestApplicationAsync(string AppID) { ResetError(); try { var strRequest = constMsGraphApplicationGet.Replace("{AppID}", AppID); var Result = await RequestAsync(strRequest, retryForbidden: true); if (Result != null) { dynamic lst = Result.Result.value; if (Result.Result.value.Count >= 1) { dynamic app = Result.Result.value[0]; var RetVal = new cMsGraphResultBase(app); return new cMsGraphResultApplication(RetVal); } return new cMsGraphResultApplication(Result); } } catch (Exception E) { cLogManager.LogException(E); } return null; } } public class cMsGraphLogonInfo { public string Tenant; public string ClientID; public string ClientSecret; public string Scope = "https://graph.microsoft.com/.default"; } public static class RequestCriticalSection { public static SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1); } public class cMsGraphResultBase { public string ID { get; private set; } = null; public string ODataId { get; private set; } = null; public string DisplayName { get; private set; } = null; public string Context { get; private set; } = null; public dynamic Result { get; private set; } = null; public cMsGraphResultBase(dynamic Result) { this.Result = Result; try { ID = Result.id; } catch { }; try { if (Result.TryGetValue("displayName", out JToken JT1)) DisplayName = Result.displayName; else if (Result.TryGetValue("name", out JToken JT2)) DisplayName = Result.name; } catch { } try { Context = Result["@odata.context"]; } catch { } ODataId = GetStringFromDynamic(Result, "@odata.id"); if (string.IsNullOrEmpty(ODataId)) ODataId = @"https://graph.microsoft.com/v1.0/directoryObjects/" + ID; } public cMsGraphResultBase(cMsGraphResultBase Result) { if (Result == null) return; this.Result = Result.Result; ID = Result.ID; ODataId = Result.ODataId; DisplayName = Result.DisplayName; Context = Result.Context; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetStringFromDynamic(dynamic O, string ProperyName) { try { return (string)O[ProperyName]; } catch { } return null; } } public class cMsGraphResultList : List { public string NextResultUrl { get; private set; } = null; public bool retryForbidden { get; private set; } = false; public cMsGraphResultList(bool retryForbidden) { this.retryForbidden = retryForbidden; } public bool hasRemaining { get { return !string.IsNullOrEmpty(NextResultUrl); } } public void AddResult(dynamic Result) { try { NextResultUrl = null; if (Result.TryGetValue("@odata.nextLink", out JToken JT)) { var JO = (JValue)JT; NextResultUrl = Result["@odata.nextLink"]; } if (Result.TryGetValue("value", out JToken JT2)) { foreach (dynamic Entry in Result.value) try { var val = new cMsGraphResultBase(Entry); this.Add(val); } catch (Exception E) { cLogManager.DefaultLogger.LogException(E); } } } catch (Exception E) { cLogManager.DefaultLogger.LogException(E); } } } public class cMsGraphResultUser : cMsGraphResultBase { public string UserPrincipalName { get; private set; } = null; public string EMail { get; private set; } = null; public string GivenName { get; private set; } = null; public string SurName { get; private set; } = null; public string JobTitle { get; private set; } = null; public string OfficeLocation { get; private set; } = null; public string PreferredLanguage { get; private set; } = null; public cMsGraphResultUser(cMsGraphResultBase Result) : base(Result) { UserPrincipalName = GetStringFromDynamic(Result.Result, "userPrincipalName"); EMail = GetStringFromDynamic(Result.Result, "mail"); GivenName = GetStringFromDynamic(Result.Result, "givenName"); SurName = GetStringFromDynamic(Result.Result, "surname"); JobTitle = GetStringFromDynamic(Result.Result, "jobTitle"); OfficeLocation = GetStringFromDynamic(Result.Result, "officeLocation"); PreferredLanguage = GetStringFromDynamic(Result.Result, "preferredLanguage"); } } public class cMsGraphCollectionUsers : Dictionary { public cMsGraphCollectionUsers() { } public cMsGraphCollectionUsers(int n) : base(n) { } public void Add(cMsGraphResultUser User) { if (!this.ContainsKey(User.ID)) this.Add(User.ID, User); } } public class cMsGraphResultApplication : cMsGraphResultBase { public string AppID { get; private set; } = null; public string PublisherDomain { get; private set; } = null; public cMsGraphResultApplication(cMsGraphResultBase Result) : base(Result) { try { AppID = Result.Result.appId; } catch { }; try { PublisherDomain = Result.Result.publisherDomain; } catch { }; } } }