As we were developing our custom engagement bot for MTC, in order to be compliant with security policies we needed to make sure all the calls to our Dynamics 365 from our Azure Functions are secured by Azure AD for our team using the bot within organization on their phone or other devices.
Since we could not use a service account Pass-Through Auth was the way to go, while implementing the code for Azure Function I hit couple of road blocks which I ended up adding this as an issue on GitHub here as we couldnt get access to auth headers.
After some digging and try and error efforts I finally managed to write a code to get this working, so if you are planning to do something similar with Dynamics 365 here it goes:
- Make sure to secure your Azure Function with Azure AD using your Org subscription
- Register a custom App within your Azure AD and give permission to access Dynamics 365
- Get Client ID and Secret for the new app
- Adding required references:
- Microsoft.IdentityModel
- Microsoft.IdentityModel.Clients.ActiveDirectory
- Next get access to right header in your Azure Function code and pass the user assertion value along with other parameter to your runTask function:
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log) { // parse query parameter string accountName = req.GetQueryNameValuePairs() .FirstOrDefault(q => string.Compare(q.Key, "AccountName", true) == 0) .Value; string data = string.Empty; IEnumerable<string> headerValues; var tokenId = string.Empty; if (req.Headers.TryGetValues("X-MS-TOKEN-AAD-ID-TOKEN", out headerValues)) { tokenId = headerValues.FirstOrDefault(); } log.Info("Results:" + tokenId); UserAssertion userAssertion = new UserAssertion(tokenId); // Get request body data = await runTask(accountName, userAssertion); return data == null ? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass an Account Name on the query string or in the request body") : req.CreateResponse(HttpStatusCode.OK, "Results:" + data); }
- Next in your runTask function using the user assertion value to create the auth token:
public static async Task<String> runTask(string accountName, UserAssertion userAssertion) { string resource = ConfigurationManager.AppSettings["Resource"]; string clientId = ConfigurationManager.AppSettings["ClientID"]; string secret = ConfigurationManager.AppSettings["Secret"]; string redirectUrl = ConfigurationManager.AppSettings["RedirectURL"]; try { //Authentication parameters received from Resource Server AuthenticationParameters ap = AuthenticationParameters.CreateFromResourceUrlAsync(new Uri(resource + "/api/data/")).Result; // Authenticate the registered application with Azure Active Directory. ClientCredential credential = new ClientCredential(clientId,secret); AuthenticationContext authContext = new AuthenticationContext(ap.Authority, false); AuthenticationResult result = await authContext.AcquireTokenAsync(ap.Resource, credential, userAssertion); using (HttpClient httpClient = new HttpClient()) { httpClient.Timeout = new TimeSpan(0, 2, 0); // 2 minutes httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken); httpClient.BaseAddress = new Uri(resource + "/api/data/v8.1/accounts?$select=name&$filter=name eq '" + accountName +"'"); httpClient.Timeout = new TimeSpan(0, 2, 0); httpClient.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0"); httpClient.DefaultRequestHeaders.Add("OData-Version", "4.0"); httpClient.DefaultRequestHeaders.Accept.Add( new MediaTypeWithQualityHeaderValue("application/json")); HttpResponseMessage response = await httpClient.GetAsync(httpClient.BaseAddress); Stream rStream= await response.Content.ReadAsStreamAsync(); StreamReader reader = new StreamReader(rStream); return reader.ReadToEnd(); } } catch (HttpRequestException e) { throw new Exception("An HTTP request exception occurred.", e); } }